/*
* Copyright (c) 2006, John D'Emic <john.demic@gmail.com>
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the University of California, Berkeley nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package net.badcurrent.ldapodm.ldap;

import net.badcurrent.ldapodm.annotations.EmbeddedLdapAttribute;
import net.badcurrent.ldapodm.annotations.LdapAttribute;
import net.badcurrent.ldapodm.typeconverter.TypeConverter;
import net.badcurrent.ldapodm.typeconverter.IntegerTypeConverter;
import net.badcurrent.ldapodm.MappingException;
import net.badcurrent.ldapodm.proxy.Proxy;
import net.badcurrent.ldapodm.reflection.ReflectionUtils;

import javax.naming.directory.Attributes;
import javax.naming.NamingException;
import java.util.Map;
import java.util.HashMap;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.annotation.Annotation;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ldap.AttributesMapper;

/**
 * <code>AttributesMapper</code> to map Attributes to an object.
 *
 *@author John D'Emic <john.demic@gmail.com>
 */
public class EntryAttributesMapper implements AttributesMapper {

    private static Log log = LogFactory.getLog(EntryAttributesMapper.class);
    /**
     * Registered map of type converters
     */
    private Map<Class, TypeConverter> typeConverters;
    /**
     * The Class being mapped to
     */
    private Class mappedClass;

    public EntryAttributesMapper() {
        
    }
    /**
     * Constructs a new <code>EntryAttributesMapper</code> for the given Class.
     *
     * @param mappedClass
     */
    public EntryAttributesMapper(Class mappedClass) {
        this.mappedClass = mappedClass;
        typeConverters = new HashMap<Class, TypeConverter>();
        typeConverters.put(Integer.class, new IntegerTypeConverter());
        typeConverters.put(int.class, new IntegerTypeConverter());
    }

    public Object mapFromAttributes(Attributes attributes) throws NamingException {
        Object object;
        try {
            object = mapAttributesToObject(attributes, mappedClass);
        } catch (IllegalAccessException e) {
            throw new MappingException(e);
        } catch (InstantiationException e) {
            throw new MappingException(e);
        } catch (InvocationTargetException e) {
            throw new MappingException(e);
        }
        return object;
    }

    Object mapAttributesToObject(Attributes attributes, Class clazz)
            throws IllegalAccessException, InstantiationException, NamingException, InvocationTargetException {

        // construct a new instance of the class we are mapping to
        Object object = Proxy.newInstance(clazz);

        for (Field field : clazz.getDeclaredFields()) {
            for (Annotation a : field.getDeclaredAnnotations()) {
                if (a instanceof LdapAttribute) {
                    // found an LdapAttribute...
                    LdapAttribute ldapAttribute = (LdapAttribute) a;
                    populateFromLdapAttribute(clazz, ldapAttribute, object, attributes,
                            field.getName());
                }
                if (a instanceof EmbeddedLdapAttribute) {
                    // found an embedded attribute
                    setEmbeddedValue(clazz, object, field.getName(), attributes);
                }
            }
        }

        // iterate through each of the methods looking for mapping annotations
        for (Method method : clazz.getDeclaredMethods()) {
            for (Annotation a : method.getDeclaredAnnotations()) {
                if (a instanceof LdapAttribute) {
                    // found an LdapAttribute...
                    LdapAttribute ldapAttribute = (LdapAttribute) a;
                    String propertyName = ReflectionUtils.getProperty(method);
                    populateFromLdapAttribute(clazz, ldapAttribute, object, attributes,
                            ReflectionUtils.getProperty(method));
                }
                if (a instanceof EmbeddedLdapAttribute) {
                    // found an embedded attribute
                    String propertyName = ReflectionUtils.getProperty(method);
                    setEmbeddedValue(clazz, object, propertyName, attributes);
                }
            }
        }
        return object;
    }

    private void populateFromLdapAttribute(Class clazz, LdapAttribute ldapAttribute,
                                         Object object, Attributes attributes,
                                         String propertyName) throws IllegalAccessException, NamingException, InvocationTargetException {
        for (String attributeValue : ldapAttribute.value()) {
            if (attributes.get(attributeValue) == null) {
                if (log.isDebugEnabled()) log.debug(attributeValue + " is null");
                continue;
            }
            // invoke the setter of the property with the Attribute from ldap...
            setValue(clazz, object, propertyName, attributes, attributeValue);
        }
    }

    private void setValue(Class clazz, Object object, String propertyName, Attributes attributes, String attributeValue)
            throws NamingException, IllegalAccessException, InvocationTargetException {
        Method setter;
        Field field;
        try {
            field = clazz.getDeclaredField(propertyName);
        } catch (NoSuchFieldException e) {
            log.warn("No field found for: " + propertyName);
            return;
        }
        try {
            setter = clazz.getMethod("set" + StringUtils.capitalize(propertyName), field.getType());
        } catch (NoSuchMethodException e) {
            log.warn("No setter found for: " + propertyName);
            return;
        }
        // if the field is a String, set the value.  If not, look for the appropriate
        // TypeConverter and invoke that...
        if (field.getType() == String.class) {
            setter.invoke(object, attributes.get(attributeValue).get());
        } else {
            if (typeConverters.get(field.getType()) == null) {
                log.error("No TypeConverter found for: " + field.getType());
                throw new MappingException("No TypeConverter found for type: " +
                        field.getType());
            }
            setter.invoke(object,
                    typeConverters.get(
                            field.getType()).fromString(
                            attributes.get(attributeValue).get().toString()
                    ));
        }
    }

    private void setEmbeddedValue(Class clazz, Object object, String propertyName, Attributes attributes) throws IllegalAccessException, NamingException, InvocationTargetException, InstantiationException {
        Field field;
        Method setter;
        // determine field and setter, then recursively invoke against new type
        try {
            field = clazz.getDeclaredField(propertyName);
            setter = clazz.getMethod("set" + StringUtils.capitalize(propertyName), field.getType());
        } catch (NoSuchFieldException e) {
            log.warn("No field found for: " + propertyName);
            return;
        } catch (NoSuchMethodException e) {
            log.warn("No setter found for: " + propertyName);
            return;
        }

        Object o = mapAttributesToObject(attributes, field.getType());
        setter.invoke(object, o);
    }
}
